package com.nononsenseapps.feeder.model

import com.nononsenseapps.feeder.parser.FeedParser
import kotlinx.coroutines.runBlocking
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.InputStream
import java.net.URL
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class FeedParserTest {
    @Rule
    @JvmField
    var tempFolder = TemporaryFolder()

    @Test
    @Throws(Exception::class)
    fun getAlternateLinksHandlesYoutube() {
        // I want this to be an Online test to make sure that I notice if/when Youtube changes something which breaks it
        runBlocking {
            val feeds: List<Pair<String, String>> =
                    FeedParser.getAlternateFeedLinksAtUrl(URL("https://www.youtube.com/watch?v=-m5I_5Vnh6A"))
            assertEquals(listOf("https://www.youtube.com/feeds/videos.xml?channel_id=UCG1h-Wqjtwz7uUANw6gazRw" to "atom"),
                    feeds)
        }

    }

    @Test
    @Throws(Exception::class)
    fun htmlAtomContentGetsUnescaped() {
        javaClass.getResourceAsStream("atom_hnapp.xml")!!
                .use {
                    val feed = FeedParser.parseFeedInputStream(URL("http://hnapp.com/rss?q=type%3Astory%20score%3E36%20-bitcoin%20-ethereum%20-cryptocurrency%20-blockchain%20-snowden%20-hiring%20-ask"), it)

                    val item = feed.items!![0]
                    assertEquals("37 – Spectre Mitigations in Microsoft's C/C++ Compiler",
                            item.title)
                    assertEquals("37 points, 1 comment",
                            item.content_text)
                    assertEquals("<p>37 points, <a href=\"https://news.ycombinator.com/item?id=16381978\">1 comment</a></p>",
                            item.content_html)
                }
    }

    @Test
    @Throws(Exception::class)
    fun enclosedImageIsUsedAsThumbnail() {
        javaClass.getResourceAsStream("rss_lemonde.xml")!!
                .use {
                    val feed = FeedParser.parseFeedInputStream(URL("http://www.lemonde.fr/rss/une.xml"), it)

                    val item = feed.items!![0]
                    assertEquals("http://s1.lemde.fr/image/2018/02/11/644x322/5255112_3_a8dc_martin-fourcade_02be61d126b2da39d977b2e1902c819a.jpg",
                            item.image)
                }
    }

    @Test
    @Throws(Exception::class)
    fun getAlternateFeedLinksDoesNotReturnRelativeLinks() {
        javaClass.getResourceAsStream("fz.html")!!
                .bufferedReader()
                .use {
                    val alts: List<Pair<String, String>> = FeedParser.getAlternateFeedLinksInHtml(it.readText())
                    assertEquals(emptyList(), alts)
                }
    }

    @Test
    @Throws(Exception::class)
    fun successfullyParsesAlternateLinkInBodyOfDocument() {
        javaClass.getResourceAsStream("nixos.html")!!
                .bufferedReader()
                .use {
                    val alts: List<Pair<String, String>> = FeedParser.getAlternateFeedLinksInHtml(it.readText(),
                            URL("https://nixos.org"))
                    assertEquals(listOf("https://nixos.org/news-rss.xml" to "application/rss+xml"), alts)
                }
    }

    @Test
    @Throws(Exception::class)
    fun getAlternateFeedLinksResolvesRelativeLinksGivenBaseUrl() {
        javaClass.getResourceAsStream("fz.html")!!
                .bufferedReader()
                .use {
                    val alts: List<Pair<String, String>> =
                            FeedParser.getAlternateFeedLinksInHtml(it.readText(),
                                    baseUrl = URL("https://www.fz.se/index.html"))
                    assertEquals(listOf(
                            "https://www.fz.se/feeds/nyheter" to "application/rss+xml",
                            "https://www.fz.se/feeds/forum" to "application/rss+xml"
                    ), alts)
                }
    }

    @Test
    @Throws(Exception::class)
    fun findsAlternateLinksReturnsNullIfNoLink() {
        val rssLink = FeedParser.findFeedUrl(atomRelative)
        assertNull(rssLink)
    }

    @Test
    @Throws(Exception::class)
    fun findsAlternateLinksReturnsAlternatesForFeedsWithAlternateLinks() {
        val rssLink = FeedParser.findFeedUrl(atomWithAlternateLinks)
        assertEquals(URL("http://localhost:1313/feed.json"), rssLink)
    }

    @Test
    @Throws(Exception::class)
    fun findsAlternateLinksPrefersAtomByDefault() {
        val rssLink = FeedParser.findFeedUrl(getCowboyHtml())
        assertEquals(URL("https://cowboyprogrammer.org/atom.xml"), rssLink)
    }

    @Test
    @Throws(Exception::class)
    fun findsAlternateLinksPreferAtom() {
        val rssLink = FeedParser.findFeedUrl(getCowboyHtml(), preferAtom = true)
        assertEquals(URL("https://cowboyprogrammer.org/atom.xml"), rssLink)
    }

    @Test
    @Throws(Exception::class)
    fun findsAlternateLinksPreferRss() {
        val rssLink = FeedParser.findFeedUrl(getCowboyHtml(), preferRss = true)
        assertEquals(URL("https://cowboyprogrammer.org/index.xml"), rssLink)
    }

    @Test
    @Throws(Exception::class)
    fun findsAlternateLinksPreferJSON() {
        val rssLink = FeedParser.findFeedUrl(getCowboyHtml(), preferJSON = true)
        assertEquals(URL("https://cowboyprogrammer.org/feed.json"), rssLink)
    }

    @Test
    @Throws(Exception::class)
    fun encodingIsHandledInAtomRss() {
        val responseBody: ResponseBody = golemDe.toResponseBody("application/xml".toMediaTypeOrNull())

        val response: Response = Response.Builder()
                .body(responseBody)
                .protocol(Protocol.HTTP_2)
                .code(200)
                .message("Test")
                .request(Request.Builder()
                        .url("https://rss.golem.de/rss.php?feed=RSS2.0")
                        .build())
                .build()

        val feed = response.use { FeedParser.parseObjectResponse(response) }

        assertEquals(true, feed?.items?.get(0)?.content_text?.contains("größte"))
    }

    // Bug in Rome which I am working around, this will crash if not worked around
    @Test
    @Throws(Exception::class)
    fun emptySlashCommentsDontCrashParsingAndEncodingIsStillRespected() {
        val responseBody: ResponseBody = emptySlashComment.toResponseBody("application/xml".toMediaTypeOrNull())

        val response: Response = Response.Builder()
                .body(responseBody)
                .protocol(Protocol.HTTP_2)
                .code(200)
                .message("Test")
                .request(Request.Builder()
                        .url("https://rss.golem.de/rss.php?feed=RSS2.0")
                        .build())
                .build()

        val feed = response.use { FeedParser.parseObjectResponse(response) }

        assertEquals(1, feed?.items?.size)
        assertEquals(true, feed?.items?.get(0)?.content_text?.contains("größte"))
    }

    @Test
    @Throws(Exception::class)
    fun correctAlternateLinkInAtomIsUsedForUrl() {
        val feed = FeedParser.parseRssAtomBytes(URL("http://utdelningsseglaren.blogspot.com/feeds/posts/default"), utdelningsSeglarenAtom)

        assertEquals("http://utdelningsseglaren.blogspot.com/2017/12/tips-pa-6-podcasts.html",
                feed.items?.get(0)?.url)
    }

    @Test
    @Throws(Exception::class)
    fun relativeLinksAreMadeAbsoluteAtom() {

        val feed = FeedParser.parseFeedInputStream(URL("http://cowboyprogrammer.org/feed.atom"), atomRelative.byteInputStream())
        assertNotNull(feed)

        assertEquals("http://cowboyprogrammer.org/feed.atom", feed.feed_url)
    }

    @Test
    @Throws(Exception::class)
    fun relativeLinksAreMadeAbsoluteAtomNoBase() {

        val feed = FeedParser.parseFeedInputStream(URL("http://cowboyprogrammer.org/feed.atom"), atomRelativeNoBase.byteInputStream())
        assertNotNull(feed)

        assertEquals("http://cowboyprogrammer.org/feed.atom", feed.feed_url)
    }

    @Test
    @Throws(Exception::class)
    fun relativeFeedLinkInRssIsMadeAbsolute() {

        val feed = FeedParser.parseRssAtomBytes(URL("https://lineageos.org/feed.xml"), lineageosRss)
        assertNotNull(feed)

        assertEquals("https://lineageos.org/", feed.home_page_url)
        assertEquals("https://lineageos.org/feed.xml", feed.feed_url)

        assertEquals("https://lineageos.org/Changelog-16", feed.items?.get(0)?.id)
        assertEquals("https://lineageos.org/Changelog-16/", feed.items?.get(0)?.url)
        assertEquals("https://lineageos.org/images/2018-02-25/lineageos-15.1-hero.png", feed.items?.get(0)?.image)
    }

    @Test
    @Throws(Exception::class)
    fun noStyles() {
        val feed = FeedParser.parseFeedInputStream(URL("http://research.swtch.com/feed.atom"), researchRsc)
        assertNotNull(feed)

        assertEquals("http://research.swtch.com/feed.atom", feed.feed_url)

        assertEquals(17, feed.items!!.size)

        val (_, _, _, title, _, _, summary, image) = feed.items!![9]

        assertEquals("http://research.swtch.com/qr-bbc.png", image)

        assertEquals("QArt Codes",
                title)

        // Style tags should be ignored
        assertEquals("QR codes are 2-dimensional bar codes that encode arbitrary text strings. A common use of QR codes is to encode URLs so that people can scan a QR code (for example, on an advertising poster, building r",
                summary)
    }

    @Test
    @Throws(Exception::class)
    fun feedAuthorIsUsedAsFallback() {
        val feed = FeedParser.parseFeedInputStream(URL("http://research.swtch.com/feed.atom"), researchRsc)
        assertNotNull(feed)

        assertEquals("http://research.swtch.com/feed.atom", feed.feed_url)

        assertEquals(17, feed.items!!.size)

        val (_, _, _, _, _, _, _, _, _, _, _, author) = feed.items!![9]

        assertEquals("Russ Cox", feed.author!!.name)
        assertEquals(feed.author, author)
    }

    @Test
    @Throws(Exception::class)
    fun geekpark() {
        val feed = FeedParser.parseFeedInputStream(URL("http://www.geekpark.net/rss"), TEST_HTML_STR.byteInputStream())

        assertNotNull(feed)

        assertNull(feed.feed_url)

        assertEquals(2, feed.items!!.size)
    }

    @Test
    @Throws(Exception::class)
    fun nixos() {
        val feed = FeedParser.parseFeedInputStream(URL("http://nixos.org/news-rss.xml"), nixosRss)
        assertNotNull(feed)

        assertNull(feed.feed_url)

        assertEquals(99, feed.items!!.size)

        val (_, _, _, title, _, _, _, image) = feed.items!![0]

        assertEquals("https://nixos.org/logo/nixos-logo-18.09-jellyfish-lores.png", image)
        assertEquals("NixOS 18.09 released", title)
    }

    @Test
    @Throws(Exception::class)
    fun nixers() {
        val feed = FeedParser.parseFeedInputStream(URL("https://newsletter.nixers.net/feed.xml"),
                nixersRss)
        assertNotNull(feed)

        assertNull(feed.feed_url)

        assertEquals(111, feed.items!!.size)

        val item = feed.items!![0]

        // Timezone issues - so only verify date
        assertTrue(message = "Expected a pubdate to have been parsed") {
            item.date_published!!.startsWith("2019-01-25")
        }
    }

    @Test
    @Throws(Exception::class)
    fun cyklist() {
        val feed = FeedParser.parseFeedInputStream(URL("http://www.cyklistbloggen.se/feed/"), cyklistBloggen)
        assertNotNull(feed)

        assertNull(feed.feed_url)

        assertEquals(10, feed.items!!.size)

        val (_, _, _, title, _, _, summary, image) = feed.items!![0]

        assertEquals("http://www.cyklistbloggen.se/wp-content/uploads/2014/01/Danviksklippan-skyltad.jpg", image)

        assertEquals("Ingen ombyggning av Danvikstull",
                title)

        // Make sure character 160 (non-breaking space) is trimmed
        assertEquals("För mer än tre år sedan aviserade dåvarande Allians-styrda Stockholms Stad att man äntligen skulle bredda den extremt smala passagen på pendlingsstråket vid Danvikstull: I smalaste passagen är gångdel",
                summary)
    }

    @Test
    @Throws(Exception::class)
    fun cowboy() {
        val feed = FeedParser.parseFeedInputStream(URL("http://cowboyprogrammer.org/index.xml"), cowboyRss)
        assertNotNull(feed)

        assertNull(feed.feed_url)

        assertEquals(15, feed.items!!.size)

        var entry = feed.items!![1]

        assertEquals("https://cowboyprogrammer.org/images/zopfli_all_the_things.jpg",
                entry.image)

        // Snippet should not contain images
        entry = feed.items!![4]
        assertEquals("Fixing the up button in Python shell history", entry.title)
        assertEquals("In case your python/ipython shell doesn’t have a working history, e.g. pressing ↑ only prints some nonsensical ^[[A, then you are missing either the readline or ncurses library. Ipython is more descri",
                entry.summary)
        // Snippet should not contain links
        entry = feed.items!![1]
        assertEquals("Compress all the images!", entry.title)
        assertEquals("Update 2016-11-22: Made the Makefile compatible with BSD sed (MacOS) One advantage that static sites, such as those built by Hugo, provide is fast loading times. Because there is no processing to be d",
                entry.summary)
    }

    @Test
    @Throws(Exception::class)
    fun rss() {
        val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = FeedParser.parseFeedInputStream(URL("https://cornucopia.cornubot.se/feeds/posts/default?alt=rss"), cornucopiaRss)

        assertEquals("http://cornucopia.cornubot.se/", home_page_url)
        assertNull(feed_url)

        assertEquals(25, items!!.size)
        val (_, _, _, title, content_html, _, summary, image, _, _, _, _, _, attachments) = items[0]

        assertEquals("Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen",
                title)
        assertEquals("Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen",
                title)

        assertEquals("För tredje månaden på raken ligger Konjunkturinsitutets barometerindikator (\"konjunkturbarometern\") kvar i överhettat läge. Det råder alltså en klart och tydligt långsiktig säljsignal i enlighet med k",
                summary)
        assertTrue(content_html!!.startsWith("För tredje månaden på raken"))
        assertEquals("https://1.bp.blogspot.com/-hD_mqKJx-XY/WLwTIKSEt6I/AAAAAAAAqfI/sztWEjwSYAoN22y_YfnZ-yotKjQsypZHACLcB/s72-c/konj.png",
                image)

        assertEquals<List<Any>?>(emptyList(), attachments)
    }

    @Test
    @Throws(Exception::class)
    fun atom() {
        val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = FeedParser.parseFeedInputStream(URL("https://cornucopia.cornubot.se/feeds/posts/default"), cornucopiaAtom)

        assertEquals("http://cornucopia.cornubot.se/", home_page_url)
        assertEquals("http://www.blogger.com/feeds/8354057230547055221/posts/default", feed_url)

        assertEquals(25, items!!.size)
        val (_, _, _, title, content_html, _, summary, image, _, _, _, _, _, attachments) = items[0]

        assertEquals("Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen",
                title)
        assertEquals("Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen",
                title)

        assertEquals("För tredje månaden på raken ligger Konjunkturinsitutets barometerindikator (\"konjunkturbarometern\") kvar i överhettat läge. Det råder alltså en klart och tydligt långsiktig säljsignal i enlighet med k",
                summary)
        assertTrue(content_html!!.startsWith("För tredje månaden på raken"))
        assertEquals("https://1.bp.blogspot.com/-hD_mqKJx-XY/WLwTIKSEt6I/AAAAAAAAqfI/sztWEjwSYAoN22y_YfnZ-yotKjQsypZHACLcB/s72-c/konj.png",
                image)

        assertEquals<List<Any>?>(emptyList(), attachments)
    }

    @Test
    @Throws(Exception::class)
    fun atomCowboy() {
        val (_, _, _, _, _, _, _, icon, _, _, _, _, items) = FeedParser.parseFeedInputStream(URL("http://cowboyprogrammer.org/atom.xml"), cowboyAtom)

        assertEquals(15, items!!.size)
        val (id, _, _, _, _, _, _, image, _, date_published) = items[1]

        assertEquals("http://cowboyprogrammer.org/dummy-id-to-distinguis-from-alternate-link", id)
        assertTrue(date_published!!.contains("2016"), "Should take the updated timestamp")
        assertEquals("http://localhost:1313/images/zopfli_all_the_things.jpg",
                image)

        assertEquals("http://localhost:1313/css/images/logo.png", icon)
    }

    @Test
    @Throws(Exception::class)
    fun morningPaper() {
        val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = FeedParser.parseFeedInputStream(URL("https://blog.acolyer.org/feed/"), morningPaper)

        assertEquals("https://blog.acolyer.org", home_page_url)
        assertNull(feed_url)

        assertEquals(10, items!!.size)
        val (_, _, _, title, _, _, _, image) = items[0]

        assertEquals("Thou shalt not depend on me: analysing the use of outdated JavaScript libraries on the web",
                title)

        assertEquals("http://1.gravatar.com/avatar/a795b4f89a6d096f314fc0a2c80479c1?s=96&d=identicon&r=G",
                image)
    }

    @Test
    @Throws(Exception::class)
    fun londoner() {
        val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = FeedParser.parseFeedInputStream(URL("http://londonist.com/feed"), londoner)

        assertEquals("http://londonist.com/", home_page_url)
        assertNull(feed_url)

        assertEquals(40, items!!.size)
        val (_, _, _, title, _, _, _, image) = items[0]

        assertEquals("Make The Most Of London's Offerings With Chip",
                title)

        assertEquals("https://assets.londonist.com/uploads/2017/06/i300x150/chip_2.jpg",
                image)
    }

    @Test
    @Throws(Exception::class)
    fun noLinkShouldFallbackToGuid() {
        val feed = FeedParser.parseFeedInputStream(URL("http://ANON.com/rss"), anon)

        assertEquals("http://ANON.com/sub", feed.home_page_url)
        assertNull(feed.feed_url)
        assertEquals("ANON", feed.title)
        assertEquals("ANON", feed.description)

        assertEquals(1, feed.items!!.size)
        val item = feed.items!![0]

        assertEquals("http://ANON.com/sub/##", item.url)

        assertEquals("ANON", item.title)
        assertEquals("http://ANON.com/sub/##", item.id)
        assertEquals("ANON", item.content_text)
        assertEquals("ANON", item.content_html)
        assertEquals("ANON", item.summary)

        /*assertEquals("2018-12-13 00:00:00",
                item.date_published)*/
    }

    fun golem2ShouldBeParsedDespiteEmptySlashComments() {
        val feed = FeedParser.parseRssAtomBytes(URL("https://rss.golem.de/rss.php?feed=RSS2.0"), golemDe2)

        assertEquals("Golem.de", feed.title)
    }

    @Test
    @Throws(Exception::class)
    fun cowboyAuthenticated() {
        runBlocking {
            val feed = FeedParser.parseObjectUrl(URL("https://test:test@cowboyprogrammer.org/auth_basic/index.xml"))
            assertEquals("Cowboy Programmer", feed?.title)
        }
    }

    @Test
    @Ignore
    @Throws(Exception::class)
    fun fz() {
        val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = FeedParser.parseFeedInputStream(URL("https://www.fz.se/feeds/nyheter"), fz)

        assertEquals("http://www.fz.se/nyheter/", home_page_url)
        assertNull(feed_url)

        assertEquals(20, items!!.size)
        val (_, _, _, title, _, _, _, image) = items[0]

        assertEquals("Nier: Automata bjuder på maffig lanseringstrailer",
                title)

        assertEquals("http://d2ihp3fq52ho68.cloudfront.net/YTo2OntzOjI6ImlkIjtpOjEzOTI3OTM7czoxOiJ3IjtpOjUwMDtzOjE6ImgiO2k6OTk5OTtzOjE6ImMiO2k6MDtzOjE6InMiO2k6MDtzOjE6ImsiO3M6NDA6IjU5YjA2YjgyZjkyY2IxZjBiMDZjZmI5MmE3NTk5NjMzMjIyMmU4NGMiO30=",
                image)
    }

    private fun getCowboyHtml(): String =
            javaClass.getResourceAsStream("cowboyprogrammer.html")!!
                    .bufferedReader()
                    .use {
                        it.readText()
                    }

    private val emptySlashComment: ByteArray
        get() = javaClass.getResourceAsStream("empty_slash_comment.xml")!!.readBytes()

    private val golemDe: ByteArray
        get() = javaClass.getResourceAsStream("golem-de.xml")!!.readBytes()

    private val golemDe2: ByteArray
        get() = javaClass.getResourceAsStream("rss_golem_2.xml")!!.readBytes()

    private val utdelningsSeglarenAtom: ByteArray
        get() = javaClass.getResourceAsStream("atom_utdelningsseglaren.xml")!!.readBytes()

    private val lineageosRss: ByteArray
        get() = javaClass.getResourceAsStream("rss_lineageos.xml")!!.readBytes()

    private val cornucopiaAtom: InputStream
        get() = javaClass.getResourceAsStream("atom_cornucopia.xml")!!

    private val cornucopiaRss: InputStream
        get() = javaClass.getResourceAsStream("rss_cornucopia.xml")!!

    private val cowboyRss: InputStream
        get() = javaClass.getResourceAsStream("rss_cowboy.xml")!!

    private val cowboyAtom: InputStream
        get() = javaClass.getResourceAsStream("atom_cowboy.xml")!!

    private val cyklistBloggen: InputStream
        get() = javaClass.getResourceAsStream("rss_cyklistbloggen.xml")!!

    private val researchRsc: InputStream
        get() = javaClass.getResourceAsStream("atom_research_rsc.xml")!!

    private val morningPaper: InputStream
        get() = javaClass.getResourceAsStream("rss_morningpaper.xml")!!

    private val fz: InputStream
        get() = javaClass.getResourceAsStream("rss_fz.xml")!!

    private val londoner: InputStream
        get() = javaClass.getResourceAsStream("rss_londoner.xml")!!

    private val anon: InputStream
        get() = javaClass.getResourceAsStream("rss_anon.xml")!!

    private val nixosRss: InputStream
        get() = javaClass.getResourceAsStream("rss_nixos.xml")!!

    private val nixersRss: InputStream
        get() = javaClass.getResourceAsStream("rss_nixers_newsletter.xml")!!
}

val atomRelative = """
<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:base='http://cowboyprogrammer.org'>
  <id>http://cowboyprogrammer.org</id>
  <title>Relative links</title>
  <updated>2003-12-13T18:30:02Z</updated>
  <link rel="self" href="/feed.atom"/>
</feed>
"""

val atomRelativeNoBase = """
<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom'>
  <id>http://cowboyprogrammer.org</id>
  <title>Relative links</title>
  <updated>2003-12-13T18:30:02Z</updated>
  <link rel="self" href="/feed.atom"/>
</feed>
"""

val atomWithAlternateLinks = """
<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom'>
  <id>http://cowboyprogrammer.org</id>
  <title>Relative links</title>
  <updated>2003-12-13T18:30:02Z</updated>
  <link rel="self" href="/feed.atom"/>
  <link rel="alternate" type="text/html" href="http://localhost:1313/" />
  <link rel="alternate" type="application/rss" href="http://localhost:1313/index.xml" />
  <link rel="alternate" type="application/json" href="http://localhost:1313/feed.json" />
</feed>
"""

val TEST_HTML_STR="""
    <rss version="2.0">
      <channel>
        <title>极客公园</title>
        <description>极客公园</description>
        <link>http://main_test.geekpark.net/rss.rss</link>
        <image>
          <url>https://imgslim.geekpark.net/images/GeekPart-blacklogo.png</url>
          <title>极客公园</title>
          <link>http://www.geekpark.net</link>
        </image>
        <item>
          <title><![CDATA[东京奥运会推迟至 2021 年举行；马斯克从中国采购 1255 台呼吸机发往洛杉矶；iOS 和 iPadOS 13.4 正式版发布]]></title>
          <link>http://www.geekpark.net/news/257421</link>
          <description><![CDATA[<p><br></p><div class="image-desc" style="text-align: center; color: #333;">
              <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/56/4b/564b6e9c9999b43c8a02598a655e1e65.png" max-width="100%" width="auto" height="auto">
            </div><h2>扩大招聘规模，字节跳动在武汉员工将增至 5000 人</h2><p>3 月 24 日消息，字节跳动宣布，今年将扩大在武汉的招聘规模，提供近 2000 个工作岗位。该招聘同时面向社会人士和在校大学生，覆盖研发、产品、人力、教育、市场、运营、设计、销售等多个方向。到 2020 年底，字节跳动在武汉员工将增至 5000 人。</p><p>据了解，字节跳动武汉分公司目前在职员工约 3000 人。其中，字节跳动武汉研发中心，是字节跳动中国继北京、深圳、上海后第 4 个研发中心，成立于 2018 年 4 月，坐落在武汉光谷地区。除研发中心外，字节跳动在武汉还拥有教育、汽车、销售、客服等多个团队。（来源：品玩）</p><p><br></p><h2>iOS 和 iPadOS 13.4 正式版发布：支持触控板操作</h2><p>3 月 25 日讯，苹果公司近日正式推送了 iOS 13.4 / iPadOS 13.4 系统更新，苹果在 iOS / iPadOS 13.4 在引入了一些新功能和更改，包括新增九款拟我表情贴纸，新增从「文件」App 共享 iCloud 云盘文件夹等功能。苹果此前发布了新版 iPad Pro 和带触控板和背光的妙控键盘，iPadOS 13.4 还新增支持触控板键盘和鼠标操作。</p><div class="image-desc" style="text-align: center; color: #333;">
              <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/65/ca/65ca297046f94ad94844c7033a63d2a3.jpg" max-width="100%" width="auto" height="auto">
            </div>
            ]]></description>
           <source>极客公园</source>
           <pubDate>Wed, 18 Mar 2020 22:32:27 +0800</pubDate>
         </item>
         <item>
               <title>
                 <![CDATA[无刘海 iPhone 12 曝光；华为 Mate 40 Pro 概念渲染外形曝光；微软发布 Office 新预览]]>
               </title>
               <link>http://www.geekpark.net/news/257365</link>
               <description>
                 <![CDATA[<p><img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/a9/4d/a94d3833b4a8d5378c6ed246d55a6167.png" max-width="100%" width="auto" height="auto" style="color: rgb(51, 51, 51); text-align: center;"><br></p><h2>F1 电子竞技虚拟大奖赛拉开帷幕 中国车手周冠宇获得冠军</h2><p>据外媒报道，F1 粉丝一定已经都知道，2020 赛季因新冠病毒大流行而被被迫推迟。然而，比赛并没有完全结束，取而代之的是在 PC 上使用官方的、来自 Codemasters 的《F1 2019》游戏。参加这场电子竞技大奖赛的车手包括在役 F1 车手、名人明星等。由于每位参赛者的游戏水平层次不齐，所以为了让这场较量更加公平，F1 电子竞技虚拟大奖赛采用了固定调校和平等的性能设定同时还关闭了车损系统。目前，F1 电子竞技虚拟大奖赛计划运行到 5 月，但实际情况还会视新冠病毒大流行的情况而定。（来源：cnBeta）</p><div class="image-desc" style="text-align: center; color: #333;">
                   <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/52/a4/52a445ef35697bacae3d4948916d283f.png" max-width="100%" width="auto" height="auto"></div><p class="image-1584981083332"></p><h2>微软发布 Office 新预览 Outlook 与 PowerPoint 迎来功能改进</h2><p>微软已经向 Insider 测试者发布了新版 Office 预览，主要带来了 Outlook 和 PowerPoint 组件的几项新功能。官方变更日志首先指出，Outlook 软件的日历模块，已迎来了视觉上的刷新。微软表示：「去年，我们为您带来了耳目一新的邮件体验。今年，终于轮到日历部分了。虽然看起来新鲜，但它还是你熟悉的日历，老用户可立即加入并提高生产力」。（来源：cnBeta）</p><p><br></p><h2>谷歌上线新冠病毒筛查网站 未来将提供检测点信息</h2><p>据外媒报道，一周前，美国总统特朗普在新闻发布会上声称谷歌正在建立一个冠状病毒筛查网站，该网站可引导人们前往病毒检测地点。当时这一宣称并未变为现实，倒是谷歌的姊妹公司 Verily 推出了一个网站，该网站仅面向湾区，并据报道只向极少数人提供了病毒测试的相关服务。不过，谷歌确实表示了将推出一个冠状病毒网站。现在，在经过短暂的延误后，谷歌最终推出了这一病毒网站。该网站的网址是：google.com/covid19。（来源：新浪科技）</p><div class="image-desc" style="text-align: center; color: #333;">
                   <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/77/c6/77c615f274ade0fd7e8681068bb1cb16.png" max-width="100%" width="auto" height="auto">
                 </div><h2>微软意外确认下一代 Windows：模块化、跨平台体验一致</h2><p>继现身 Geekbench 后，Windows Core OS（WCOS）甚至出现在了微软官方支持文档中。文档中，微软无意中确认了桌面字体对 Windows Core OS 的支持。令人玩味的是，该文档很快被修改，删除了「WCOS」和未来设备等内容。</p><p>Windows Core OS 是 Windows 的精简版本，Surface Neo、Surface Hub 2S、Hololens 使用 Windows 10X 均基于它开发。外界预计，微软会在今年 5 月举行的线上 开发者大会（Build 2020）公布新进度。（来源：快科技）</p><p><br></p><h2>中国联通：精简套餐 400 余个 整治虚拟运营商骚扰电话</h2><p>近日，中国联通党组公布了巡视整改进展情况。中国联通方面称，将积极解决套餐数量多、看不懂、选择难的问题。持续加大线上线下产品清理力度，及时下架不符合市场需求产品，集中整改期间，精简套餐 400 余个；简洁、全面公示在售套餐完整内容，优化套餐规则，让用户明明白白消费。中国联通还将加快解决骚扰电话投诉多、不知情定制投诉，特别是整治虚拟运营商骚扰电话偏多问题。（来源：新浪科技）</p><div class="image-desc" style="text-align: center; color: #333;">
                   <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/24/a8/24a8056bfaacc914e2fcb14b512e6c29.png" max-width="100%" width="auto" height="auto"></div><p class="image-1584981167477"></p><h2>被难到放弃的苹果 AirPower 被曝项目重启：推翻重做解决问题</h2><p>近日，分析师 Jon Prosser 爆料称，苹果的 AirPower 再次处于活跃状态，他表示苹果已经开始制造 AirPower 的原型产品。Jon Prosser 表示目前苹果工程师正在重新设计充电线圈，并将原来 AirPower 外部硅材质改为了白色皮革，以便解决散热问题。他还透露了此前砍掉 AirPower 的原因，当时 Apple Watch 无法在各类 AirPower 原型机上使用，苹果也拒绝发布一款无法搭配 Apple Watch 使用的 AirPower，所以要求工程师推翻重做。（来源：快科技）</p><p><br></p><h2>苹果终于搞定了？无刘海 iPhone 12 曝光</h2><p>今日，前魅蓝总裁李楠在与网友交流时谈到，iPhone 9 推迟到了 4 月中旬发布。另外，iPhone 今年预计有多达五款新机。讨论中还谈到 iPhone 12 有无刘海的问题，李楠称苹果一直在努力但好像这一代没搞定。不过，外媒又扒出一些 iPhone 12 无刘海的证据，甚至据此制作了渲染图。关于 iPhone 12 的发布时间，分析师兼爆料人 Jon Prosser 的消息是，5G iPhone 12 要推迟到 11 月才能与大家见面。（来源：快科技）</p><p><br></p><h2>郭明錤：iPhone 12 镜头全系从 6P 升级 7P</h2><p>天风国际旗下分析师郭明錤给出的报道称，疫情不会影响苹果已经为接下来几代 iPhone 制定的发展路线，而具体到今年的情况就是，iPhone 12 的广角镜头将从之前的 6P 升级至 7P，而尺寸达到了 1/1.9，并支持 Sensor shift。（来源：快科技）</p><div class="image-desc" style="text-align: center; color: #333;">
                   <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/3a/f6/3af6747e8b548250acd2aefa3c133f73.jpg" max-width="100%" width="auto" height="auto"></div><h2>华为 Mate 40 Pro 概念渲染外形曝光</h2><p>本周四（ 3 月 26 日），华为上半年的扛鼎旗舰 P40 系列就将盛大发布。无论是从官方预热还是借助各方爆料，对于该系列新机，我们已经了解甚多。P 系列在华为的定位中主打风尚设计和极致影像，与下半年 Mate 系列的前沿科技、全能商务有重合也有区别。日前，<strong>网上流出一组 Mate 40 Pro渲 染图，外形相当别致。</strong></p><div class="image-desc" style="text-align: center; color: #333;">
                   <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/25/35/2535889bfe68784258c5a5ae88c867e6.png" max-width="100%" width="auto" height="auto"></div><p class="image-1584981997386"></p><p class="image-1584981581433"></p><div class="image-desc" style="text-align: center; color: #333;">
                   <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/00/50/00508895446168f382a252ae9b758581.png" max-width="100%" width="auto" height="auto">
                 </div><h2>IBM 超级计算机筛选出 77 种抗病毒化合物，成抗疫新力量</h2><p>在这场抗疫竞赛的背后，超级计算机成为了加速器。据 Venture beat 报道，IBM 加入了由美国政府主导成立的 COVID-19 高性能计算联盟（High Performance Computing，HPC），帮助研究人员部署预测模型，分析新冠疫情的发展过程，加速推进病毒治疗进程。</p><p>据悉，IBM 此次参与 COVID-19 研究的是 Summit 超级计算机，是目前世界上最强大的超级计算机。Summit 还拥有超过 10PB 的存储器，其计算能力最高可达 200PFlops。（来源：雷锋网）</p><p><br></p><h2>奥迪计划在其英戈尔施塔特工厂附近建立电池组装厂</h2><p>德国大众汽车集团子公司奥迪计划在其英戈尔施塔特工厂附近建立一家电池组装厂。与此同时，该公司宣布将对该工厂进行投资，以确保电动汽车产量快速增长。目前，奥迪正在加速转型。该公司转型是为了夺回被梅赛德斯-奔驰和宝马夺走的「地盘」，并抗衡来自特斯拉的压力。（来源：TechWeb）</p><div class="image-desc" style="text-align: center; color: #333;">
                   <img class="uploaded-img" src="https://imgslim.geekpark.net/uploads/image/file/8f/86/8f86d53efc5baf389e55b211dc517ab9.png" max-width="100%" width="auto" height="auto">
                 </div><h2>Omida：2020 年将成为所有人的 5G 之年</h2><p>来自市场研究公司 Omdia（原 Ovum，现已更名）的最新研究报告表示，尽管行业对用例和覆盖能力存在一些担忧，5G 将成为用户采用速度最快的一代移动技术，预计到 2020 年底用户数将达到 6300 万。这一历史性增长完全归结于两个因素——全球范围内推出 5G 网络的运营商数量增加，以及 5G 智能手机和其他 5G 终端设备价格的迅速下降。</p><p>5G modem 本身存在着激烈的竞争。九年前，高通几乎垄断了 4G modem，而如今，三星和华为都拥有自己旗下芯片公司生产的 5G modem。在 2019 年收购了英特尔 modem 业务之后，苹果可能也会加入他们打造自己的 5G 芯片，而联发科也已将开始销售 5G modrm 并为其他制造商提供选择。Omdia 消费技术首席分析师 Daniel Gleeson 表示，这场竞争将是未来几年 5G 价格暴跌的主要原因。</p><p>Omdia 预计，5G 将对智能手机销售带来小幅提振，但智能手机市场已经接近饱和，如果制造商和运营商想要获得增长，就必须考虑非智能手机设备。使额外的网络连接所产生的溢价更更易承担，将成为说服消费者的一个重要因素。（来源：C114 中国通信网）</p><p><br></p>]]>
               </description>
               <source>极客公园</source>
               <pubDate>Tue, 24 Mar 2020 08:27:15 +0800</pubDate>
         </item>
       </channel>
    </rss>
""".trimIndent()